/** This is the JTalker server 
 *  @author ChrM */

package com.jtalker.server;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.util.ArrayList;

import javax.net.ssl.SSLServerSocketFactory;

import com.jtalker.server.JTalkerParser.GlobalUser;

public class JTalkerServer {
	public static final String RELEASE = "0.7.1";
	public static final String CHATROOM_MARKER = "#";
	public static final String GLOBAL_USER_LIST = "users.xml";

	public JTalkerServer(int prorNum, String pluginDir, boolean useSSL, String logFile, String workDir) {
		this.serverSocket = null;
		this.listening = true;
		this.portNum = prorNum;
		this.useSSL = useSSL;
		this.workDir = workDir;
		this.myLogger = new Logger(logFile);

		if (!this.loadGlobalUsers()) {
			this.getLogger().writeError(
					"Can't load global user file (" + this.workDir + GLOBAL_USER_LIST + "). No user defined.");
			// System.exit(-1);
		}
		// Show all available PlugIns
		this.listPlugIns(pluginDir);
		// Start updater
		this.uut = new UsersUpdateTimer(this);
		this.uut.start();

		if (this.useSSL) {
			this.getLogger().write("+ Creating secure socket on port " + this.portNum);
		} else {
			this.getLogger().write("+ Creating socket on port " + this.portNum);
		}
		try {
			if (this.useSSL) {
				this.serverSocket = SSLServerSocketFactory.getDefault().createServerSocket(this.portNum);
			} else {
				this.serverSocket = new ServerSocket(this.portNum);
			}
		} catch (IOException _e) {
			this.getLogger().writeError("*** Could not listen on port: " + this.portNum);
			this.getLogger().writeError("*** Error: " + _e.toString());
			System.exit(-1);
		}

		this.getLogger().write("+ OK. Waiting for clients...");

		while (this.listening) {
			try {
				// Create Thread
				new JTalkerServerThread(this, this.serverSocket.accept()).start();
			} catch (Exception e) {
				this.getLogger().writeError("*** Runtime Error: Can't create thread: " + e.getMessage());
				e.printStackTrace();
			}
		}

		try {
			this.serverSocket.close();
		} catch (Exception e) {
			this.getLogger().writeError("*** Runtime Error: Can't close socket: " + e.getMessage());
		}
	}

	/** Gets the logger */
	public Logger getLogger() {
		return this.myLogger;
	}

	/** Print out connected members as XML */
	public synchronized String getXMLMemberList(JTalkerMember caller) {
		String retVal = "";
		for (int i = 0; i < this.memberList.size(); i++) {
			JTalkerMember member = this.memberList.get(i);
			if (member != null) {
				if (!member.isChatroom()) {
					retVal += ("<MEMBER name=\"" + member.getUserName() + "\"");
					retVal += (" status=\"" + member.getUserStatus() + "\"");
					retVal += (" ip=\"" + member.getUserIP() + "\"/>");
				} else {
					if (member.isChatroomUser(caller.getUserName())) {
						retVal += ("<MEMBER name=\"" + member.getUserName() + "\"");
						retVal += (" status=\"" + member.getUserStatus() + "\"");
						retVal += (" type=\"" + JTalkerMember.USER_TYPE_CHATROOM + "\"");
						retVal += (" ip=\"" + member.getUserIP() + "\"/>");
					}
				}
			}
		}
		return retVal;
	}

	/** Gets number of connected members */
	public synchronized int getNumOfMembers() {
		return this.memberList.size();
	}

	/** Removes member by port number from the member list */
	public synchronized boolean removeMember(int port) {
		for (int i = 0; i < this.memberList.size(); i++) {
			if ((this.memberList.get(i)).getPort() == port) {
				this.memberList.remove(i);
				return true;
			}
		}
		// Socket not found
		return false;
	}

	/** Removes member from the member list */
	public synchronized boolean removeMember(JTalkerMember member) {
		for (int i = 0; i < this.memberList.size(); i++) {
			if (this.memberList.get(i).getUserName().equals(member.getUserName())) {
				this.memberList.remove(i);
				return true;
			}
		}
		// Member not found
		return false;
	}

	/** Removes a chatroom */
	public boolean removeChatroom(String roomName, JTalkerMember caller) {
		// find chatroom
		if (!roomName.startsWith(CHATROOM_MARKER)) {
			roomName = CHATROOM_MARKER + roomName;
		}
		JTalkerMember member = this.getMember(roomName);
		if (member == null) {
			return false;
		}
		if (!member.isChatroom()
				|| ((member.getChatroomAdmin() != null && !member.getChatroomAdmin().equals(caller.getUserName()) && caller
						.getUserRoleValue() < 1))) {
			return false;
		}
		this.getLogger().write("+ Removing Chatroom: " + roomName + " by '" + caller.getUserName() + "'");
		if (this.removeMember(member)) {
			this.sendUpdateRequestToAll(null);
			return true;
		}

		return false;
	}

	/** Adds a new User to a chatroom */
	public boolean addChatroomUser(String roomName, String newUser, JTalkerMember caller) {
		// find chatroom
		if (!roomName.startsWith(CHATROOM_MARKER)) {
			roomName = CHATROOM_MARKER + roomName;
		}
		JTalkerMember member = this.getMember(roomName);
		if (member == null || !member.isChatroom()
				|| (!member.getChatroomAdmin().equals(caller.getUserName()) && caller.getUserRoleValue() < 1)) {
			return false;
		}
		if (newUser != null && JTalkerServer.isAllowedUserName(newUser)) {
			this.getLogger().write(
					"+ Adding user '" + newUser + "' to chatroom " + roomName + ". Caller: " + caller.getUserName());

			// Add user and send update request to all chatroom users
			if (member.addChatroomUser(newUser)) {
				this.sendUpdateRequestTo(member.getChatroomUsers());
			} else {
				return false;
			}
		}

		return true;
	}

	/** Removes a new User to a chatroom */
	public boolean removeChatroomUser(String roomName, String userName, JTalkerMember caller) {
		// find chatroom
		if (!roomName.startsWith(CHATROOM_MARKER)) {
			roomName = CHATROOM_MARKER + roomName;
		}
		JTalkerMember member = this.getMember(roomName);
		if (member == null || !member.isChatroom()
				|| (!member.getChatroomAdmin().equals(caller.getUserName()) && caller.getUserRoleValue() < 1)) {
			return false;
		}
		// Don't remove the operator.
		if (member.getChatroomAdmin().equals(userName)) {
			return false;
		}
		if (userName != null && JTalkerServer.isAllowedUserName(userName)) {
			this.getLogger().write(
					"+ Removing user '" + userName + "' from chatroom " + roomName + ". Caller: "
							+ caller.getUserName());

			// remove user from chat-room and send update request to all users
			if (member.removeChatroomUser(userName)) {
				this.sendUpdateRequestTo(member.getChatroomUsers());
			} else {
				return false;
			}
		}

		return true;
	}

	/** Gets all chatroom users as ";;" separated String */
	public String getChatroomUsers(String roomName, JTalkerMember caller) {
		// find chatroom
		if (!roomName.startsWith(CHATROOM_MARKER)) {
			roomName = CHATROOM_MARKER + roomName;
		}
		JTalkerMember jtm = this.getMember(roomName);
		if (jtm == null || !jtm.isChatroom()) {
			return "";
		}
		String[] allCRUsers = jtm.getChatroomUsers();
		String retVal = "";

		if (allCRUsers == null) {
			return retVal;
		}
		for (int i = 0; i < allCRUsers.length; i++) {
			retVal += allCRUsers[i];
			if (i < (allCRUsers.length - 1)) {
				retVal += ";;";
			}
		}

		return retVal;
	}

	/**
	 * Checks for ivalid session IDs
	 * 
	 * @return True, if one or more users with ivalid session IDs are removed
	 */
	public synchronized boolean cleanUpMemberList() {
		boolean retVal = false;

		for (int i = 0; i < this.memberList.size(); i++) {
			JTalkerMember tmpMem = this.memberList.get(i);
			if (tmpMem != null && !tmpMem.isSessionValid() && !tmpMem.isOnline) {
				// Remove user from the list of online users
				this.getLogger().write("+ Removing user (closed session): " + tmpMem.getUserInfo());
				this.memberList.remove(i);
				retVal = true;
			}
		}

		return retVal;
	}

	/** Redirects messages from the chat-rooms to all allowed users */
	public synchronized boolean redirectChatroomMessages() {
		// TODO: check this method on daylight ;-)
		for (int i = 0; i < this.memberList.size(); i++) {
			JTalkerMember memeber = this.memberList.get(i);
			if (memeber != null && memeber.isChatroom()) {
				// all Messages from the chat room
				JTalkerMember.Message[] tmpMess = memeber.getMessages();
				for (int j = 0; j < memeber.getChatroomUsers().length; j++) {
					JTalkerMember tmpMem = this.getMember(memeber.getChatroomUsers()[j]);
					if (tmpMem != null) {
						tmpMem.addMessages(tmpMess);
					}
				}
			}
		}

		return true;
	}

	/**
	 * Checks user login and adds this user to the list or verified the session ID
	 * 
	 * @return The new created user or null if this method fails
	 */
	public synchronized JTalkerMember addMember(String userName, String userPasswd, Socket socket, String type,
			String sid) {
		String tmpIPAddr = "" + socket.getInetAddress().getHostAddress();
		String tmpPort = "" + socket.getPort();
		if (userName != null && userPasswd != null) {
			if (this.checkIfRegistered(userName, userPasswd) && !this.isUserOnlineByName(userName)) {
				JTalkerMember member = new JTalkerMember(userName, tmpIPAddr, tmpPort, "normal", type);
				if (this.memberList.add(member)) {
					JTalkerMember newMember = this.getMember(userName);
					/** Copy global settings to this object. */
					newMember.setUserRole(this.getGlobalUserByName(userName).getRole());
					return this.getMember(userName);
				}
			}
		} else if (sid != null) {
			if (this.isUserOnlineBySID(sid)) {
				this.getMemberBySID(sid).updateConnectionData(tmpIPAddr, tmpPort);
				return this.getMemberBySID(sid);
			}
		}

		return null;
	}

	/**
	 * Adds a member to the list as a "chat-room"
	 * 
	 * @return The new created member or null if this method fails
	 */
	public synchronized JTalkerMember addChatRoom(String userName) {
		if (userName != null && JTalkerServer.isAllowedUserName(userName)) {
			userName = CHATROOM_MARKER + userName;

			// Check if not already in use
			if (this.getMember(userName) != null) {
				return null;
			}
			this.getLogger().write("+ Creating Chatroom: " + userName);

			if (this.memberList.add(new JTalkerMember(userName, null, null, null, JTalkerMember.USER_TYPE_CHATROOM))) {
				return this.getMember(userName);
			}
		}

		return null;
	}

	/** Gets Member by socket */
	public synchronized JTalkerMember getMember(Socket socket) {
		for (int i = 0; i < this.memberList.size(); i++) {
			if (this.memberList.get(i).getPort() == socket.getPort()) {
				return this.memberList.get(i);
			}
		}
		// Socket not found
		return null;
	}

	/** Gets Member by name */
	public synchronized JTalkerMember getMember(String name) {
		for (int i = 0; i < this.memberList.size(); i++) {
			if (this.memberList.get(i).getUserName().equals(name)) {
				return this.memberList.get(i);
			}
		}
		// Name not found
		return null;
	}

	/** Gets Member by session ID */
	public synchronized JTalkerMember getMemberBySID(String sid) {
		for (int i = 0; i < this.memberList.size(); i++) {
			if ((this.memberList.get(i)).getSessionID().equals(sid)) {
				return this.memberList.get(i);
			}
		}
		// Name not found
		return null;
	}

	/** Checks if the user is already online (By user name) */
	private synchronized boolean isUserOnlineByName(String userName) {
		for (int i = 0; i < this.memberList.size(); i++) {
			if (this.memberList.get(i).getUserName().equals(userName)) {
				return true;
			}
		}
		return false;
	}

	/** Checks if the user is already online (By session ID) */
	private synchronized boolean isUserOnlineBySID(String sid) {
		for (int i = 0; i < this.memberList.size(); i++) {
			if ((this.memberList.get(i)).getSessionID().equals(sid)) {
				return true;
			}
		}
		return false;
	}

	/** Sends an update request to all active users */
	public boolean sendUpdateRequestToAll(String sender) {
		for (int i = 0; i < this.memberList.size(); i++) {
			try {
				// Do not send to the causer
				if (sender != this.memberList.get(i).getUserName()) {
					this.memberList.get(i).sendUpdateRequest();
				}
			} catch (Exception e) {
				// Something went wrong
				this.getLogger().writeError(
						"*** Runtime Error: Can't send update request to one member: " + e.getMessage());
			}
		}
		return true;
	}

	/** Sends an update request to a list of active users */
	public boolean sendUpdateRequestTo(String users) {
		String[] tmpUsers = users.split(",");
		return this.sendUpdateRequestTo(tmpUsers);
	}

	/** Sends an update request to a list of active users */
	public boolean sendUpdateRequestTo(String[] users) {
		for (int i = 0; i < users.length; i++) {
			if (this.getMember(users[i]) != null) {
				this.getMember(users[i]).sendUpdateRequest();
			}
		}
		return true;
	}

	/** Checks if user is a registered one */
	private boolean checkIfRegistered(String name, String passwd) {
		GlobalUser tmpUser = null;
		// Creates an MD5 from given password and checks it against the global
		// user list.
		for (int i = 0; i < this.globalUsers.size(); i++) {
			tmpUser = this.globalUsers.get(i);
			if (tmpUser.getName().equals(name) && tmpUser.getPasswd().equals(JTalkerServer.makeHash(passwd)))
				return true;
		}

		this.getLogger().write("+ Unregistered user tries to connect: " + name + "/" + passwd);
		return false;
	}

	/** Checks if user is a registered one (only by name) */
	private boolean checkIfRegistered(String _name) {
		/*
		 * GlobalUser tmpUser = null;
		 * 
		 * for( int i = 0; i < this.globalUsers.size(); i++ ) { tmpUser = (GlobalUser)this.globalUsers.get(i);
		 * if(tmpUser.getName().equals(_name)) return true; }
		 */

		if (this.getGlobalUserByName(_name) != null)
			return true;

		this.getLogger().write("+ Unregistered user tries to connect: " + _name);
		return false;
	}

	/** Adds global user on the fly */
	public boolean addGlobalUser(String name, String passwd) {
		if (name == null || passwd == null) {
			return false;
		}
		if (this.checkIfRegistered(name)) {
			return false;
		}
		if (name.indexOf(" AT ") != -1 || name.indexOf("\"") != -1) {
			return false;
		}
		// add this user to global list
		JTalkerParser tmpJtp = new JTalkerParser();
		this.getLogger().write("+ Try to create a new account...");
		this.globalUsers.add(tmpJtp.createGlobalUserObject(name, passwd, null, null));
		// this.writeGlobalUsersFile();
		this.getLogger().write("+ New account successfull created: " + name + "/" + JTalkerServer.makeHash(passwd));
		return true;
	}

	/** Gets a global user by name */
	public GlobalUser getGlobalUserByName(String name) {
		GlobalUser tmpUser = null;
		for (int i = 0; i < this.globalUsers.size(); i++) {
			tmpUser = this.globalUsers.get(i);
			if (tmpUser.getName().equals(name)) {
				return tmpUser;
			}
		}
		return null;
	}

	/** Check for PlugIns */
	private void listPlugIns(String plugInDir) {
		this.getLogger().write("+ Using following PlugIns:");
		// TODO: Use default plugin dir or null
		if (myPlugInLoader == null) {
			myPlugInLoader = new JTalkerPlugInLoader(this, plugInDir);
		}
		this.getLogger().write("  " + myPlugInLoader.getLoadedPlugIns());
	}

	/** Gets the PlugInLoader object */
	public JTalkerPlugInLoader getPlugInLoader() {
		return myPlugInLoader;
	}

	/**
	 * Gets List of all GlobalUsers.
	 * 
	 * @return ";;" - separated String with all global users
	 */
	public String getAllGlobalUsers() {
		String tmpRetVal = "";
		int l = this.globalUsers.size();

		for (int i = 0; i < l; i++) {
			tmpRetVal += this.globalUsers.get(i).getName();
			if (i < (l - 1)) {
				tmpRetVal += ";;";
			}
		}
		return tmpRetVal;
	}

	/** Loads the Global User List */
	private boolean loadGlobalUsers() {
		try {
			File myUsers = new File(this.workDir + GLOBAL_USER_LIST);
			JTalkerParser myParser = new JTalkerParser();

			if (!myParser.parsing(myUsers)) {
				return false;
			}
			this.globalUsers.clear();

			for (int i = 0; i < myParser.getGlobalUserNum(); i++) {
				this.globalUsers.add(myParser.getAGlobalUser(i));
				// System.out.println("+ " + myParser.getAGlobalUser(i).name);
			}

			this.getLogger().write("+ Number of global users: " + this.globalUsers.size());

		} catch (Exception e) {
			return false;
		}

		return true;
	}

	/**
	 * Static method for generating hashes
	 * 
	 * @param _input
	 *            Input String
	 * @return Hash value or the input String if this fails
	 */
	public static String makeHash(String _input) {
		try {
			MessageDigest md5 = MessageDigest.getInstance("MD5");
			md5.update(_input.getBytes());
			java.math.BigInteger hash = new java.math.BigInteger(1, md5.digest());
			return hash.toString(16);
		} catch (Exception _e) {
			return _input;
		}
	}

	/**
	 * Static method for checking for allowed user names
	 * 
	 * @param input
	 *            Input String for checking
	 * @return True, if the input is an allowed user name
	 */
	public static boolean isAllowedUserName(String input) {
		if (input.length() > 10) {
			return false;
		}
		return input.matches("[A-Za-z0-9-]+");
	}

	// Private members
	private ServerSocket serverSocket;
	private boolean listening;
	private int portNum;
	private boolean useSSL = false;
	private ArrayList<JTalkerMember> memberList = new ArrayList<JTalkerMember>();
	private ArrayList<GlobalUser> globalUsers = new ArrayList<GlobalUser>();
	private UsersUpdateTimer uut = null;
	private JTalkerPlugInLoader myPlugInLoader = null;
	private String workDir = "./";

	// Test method
	public static String test(String _in) {
		return "Hallo!";
	}

	// Static main methode
	public static void main(String[] _args) throws IOException {
		// Greeting message
		System.out.println("\n------    Welcome to JTalker-Server (" + RELEASE + ")    ------");
		System.out.println("------ (c)2003-2009 by ChrM (http://chrm.info) ------\n");

		if (!parseParameters(_args)) {
			System.err
					.println("\nUSAGE: JTalkerServer [-p PORT_NUMBER] [-x PLUGIN_DIR/] [-s|--ssl] [-l LOGFILE] [-w WORKDIR/] [-h|--help]");
			System.err
					.println("If you want to use SSL, Java params 'javax.net.ssl.keyStore' and 'javax.net.ssl.keyStorePassword' must be set!\n\n");
			return;
		}

		@SuppressWarnings("unused")
		JTalkerServer theServer = new JTalkerServer(PARAMS_PORT, PARAMS_PLUGIN_DIR, USE_SSL, PARAMS_LOGFILE,
				WORKING_DIR);
		System.out.println("\n------      JTalker-Server is going down. Bye...   ------\n");
	}

	/** Parses program params */
	private static boolean parseParameters(String[] params) {
		for (int i = 0; i < params.length; i++) {
			System.out.println(params[i]);
			if (params[i].equals("-p")) {
				// parsing port number
				try {
					PARAMS_PORT = Integer.parseInt(params[i + 1]);
				} catch (Exception _e) {
					return false;
				}
			} else if (params[i].equals("-x")) {
				try {
					PARAMS_PLUGIN_DIR = params[i + 1];
					if (!new File(PARAMS_PLUGIN_DIR).isDirectory()) {
						System.err.println("*** Plugin directory doesn't exists: " + PARAMS_PLUGIN_DIR);
						return false;
					}
				} catch (Exception _e) {
					return false;
				}
			} else if (params[i].equals("-l")) {
				try {
					PARAMS_LOGFILE = params[i + 1];
				} catch (Exception _e) {
					return false;
				}
			} else if (params[i].equals("-w")) {
				try {
					WORKING_DIR = params[i + 1];
					if (!(new File(WORKING_DIR + GLOBAL_USER_LIST).canWrite())) {
						System.err
								.println("*** Working dir problem: Can't write in: " + WORKING_DIR + GLOBAL_USER_LIST);
						return false;
					}
				} catch (Exception _e) {
					return false;
				}
			} else if (params[i].equals("-s") || params[i].equals("--ssl")) {
				// Check for keystore availability
				if (checkForSSLStuff())
					USE_SSL = true;
				else {
					USE_SSL = false;
					System.err.println("*** Can't use SSL.");
					return false;
				}
			} else if (params[i].equals("-h") || params[i].equals("--help")) {
				String tmpOut = "\nUniversal communication server with plugin extensions.";
				tmpOut += "\n\nParameters:";
				tmpOut += "\n -p <PORT_NUMBER>  Listen on this port for client connections (default: 4444)";
				tmpOut += "\n -x <PLUGIN_DIR>   Search in this directory for server plugins (extensions)";
				tmpOut += "\n -l <LOGFILE>      Write logs to <LOGFILE> instead of stdout.";
				tmpOut += "\n -w <WORKDIR>      Working Directory. User config will be stored here.";
				tmpOut += "\n -s, --ssl         Use secure sockets (SSL). Needs a keystore and some java-vars to be set!";
				tmpOut += "\n -h, --help        Show this help text and exit";
				System.out.println(tmpOut);
				return false;
			}
		}

		return true;
	}

	/** Checks if all SSL stuff is available */
	private static boolean checkForSSLStuff() {
		try {
			String keyStore = System.getProperty("javax.net.ssl.keyStore");
			if (keyStore != null) {
				boolean keyStoreOK = new File(keyStore).canRead();
				if (!keyStoreOK)
					System.err.println("*** Can't read " + keyStore);
				return keyStoreOK;
			} else {
				System.err.println("*** 'javax.net.ssl.keyStore' not set!");
			}
		} catch (Exception _e) {
			System.err.println("*** Error: " + _e.toString());
			return false;
		}

		return false;
	}

	/**
	 * Class for asynchronize updating the global users file and checking for valid session IDs
	 */
	public class UsersUpdateTimer extends Thread {
		public JTalkerServer parent = null;
		public boolean terminate = false;
		private int lastLength = 0; // Last number of global users

		public UsersUpdateTimer(JTalkerServer parent) {
			this.parent = parent;
			this.lastLength = parent.globalUsers.size();
		}

		private synchronized boolean writeXMLFile() {
			try {
				File outputFile = new File(this.parent.workDir + GLOBAL_USER_LIST);
				FileWriter out = new FileWriter(outputFile);
				// Write Head
				out.write("<?xml version=\"1.0\"?>\n\n<users>\n");
				for (int i = 0; i < this.parent.globalUsers.size(); i++) {
					GlobalUser tmpGU = (GlobalUser) this.parent.globalUsers.get(i);
					out.write("  <global_user name=\"" + tmpGU.getName() + "\" passwd=\"" + tmpGU.getPasswd()
							+ "\" role=\"" + tmpGU.getRole() + "\" contacts=\"" + tmpGU.getContacts() + "\" />\n");
				}
				out.write("</users>");
				out.close();
				return true;

			} catch (Exception e) {
				return false;
			}
		}

		public void run() {
			// boolean firstRun = true;
			while (!terminate) {
				if (this.lastLength != this.parent.globalUsers.size()) {
					this.writeXMLFile();
					this.lastLength = this.parent.globalUsers.size();
				}
				// Clean up user List (kill old sessions)
				this.parent.cleanUpMemberList();
				try {
					Thread.sleep(5000); // Update every 5 seconds.
				} catch (Exception e) {
				}
			}
		}
	}

	// Logger
	private Logger myLogger = null;

	// Set defaults (will be overwritten by users params)
	public static int PARAMS_PORT = 4444;
	public static String PARAMS_PLUGIN_DIR = "D:\\Studying\\workspace\\Chat\\bin\\com\\jtalker\\server\\plugins\\";
	public static String PARAMS_LOGFILE = null;
	public static String WORKING_DIR = "./";
	public static boolean USE_SSL = false;
}
